// 版权所有2010 Go作者。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

/*
Package zip provides support for reading and writing ZIP archives.

See: https:// www.pkware。com/appnote 

This package does not support disk spanning.

A note about ZIP64:

To be backwards compatible the FileHeader has both 32 and 64 bit Size
fields. The 64 bit fields will always contain the correct value and
for normal archives both fields will be the same. For files requiring
the ZIP64 format the 32 bit fields will be 0xffffffff and the 64 bit
fields must be used instead.
*/
package zip

import (
	"io/fs"
	"path"
	"time"
)

// 压缩方法。
const (
	Store   uint16 = 0 // 无压缩
	Deflate uint16 = 8 // 放气压缩
)

const (
	fileHeaderSignature      = 0x04034b50
	directoryHeaderSignature = 0x02014b50
	directoryEndSignature    = 0x06054b50
	directory64LocSignature  = 0x07064b50
	directory64EndSignature  = 0x06064b50
	dataDescriptorSignature  = 0x08074b50 // 事实标准；OS X Finder要求
	fileHeaderLen            = 30         // /+文件名+额外
	directoryHeaderLen       = 46         // /+文件名+额外+注释
	directoryEndLen          = 22         // /+注释
	dataDescriptorLen        = 16         // /四个单元32：描述符签名，crc32，压缩大小，大小
	dataDescriptor64Len      = 24         // /两个单元32：签名，crc32 |两个单元64：压缩大小，大小
	directory64LocLen        = 20         // 
	directory64EndLen        = 56         // /+额外

	// CreatorVersion中第一个字节的常量。
	creatorFAT    = 0
	creatorUnix   = 3
	creatorNTFS   = 11
	creatorVFAT   = 14
	creatorMacOSX = 19

	// 版本号。
	zipVersion20 = 20 // 2.0 
	zipVersion45 = 45 // 4.5（读取和写入zip64档案）

	// 非zip64档案的限制。
	uint16max = (1 << 16) - 1
	uint32max = (1 << 32) - 1

	// 额外的头ID。
	// 
	// IDs 0。。31个保留供PKWARE官方使用。
	// 超过该范围的ID由第三方供应商定义。
	// 由于ZIP缺少高精度的时间戳（也没有用于日期字段的时区的官方规范
	// ），许多相互竞争的额外字段
	// 已经发明出来。普遍使用有效地使它们成为“官方的”。
	// 
	// 参见http:
	zip64ExtraID       = 0x0001 // Zip64扩展信息
	ntfsExtraID        = 0x000a // NTFS 
	unixExtraID        = 0x000d // UNIX 
	extTimeExtraID     = 0x5455 // 扩展时间戳
	infoZipUnixExtraID = 0x5855 // Info ZIP UNIX扩展
)

// 文件头描述ZIP文件中的文件。
// 有关详细信息，请参阅zip规范。
type FileHeader struct {
	// Name是文件名。
	// 
	// 必须是相对路径，不能以驱动器号（例如“C:”）开头，
	// 并且必须使用正斜杠而不是反斜杠。尾随的斜杠
	// 表示该文件是一个目录，不应包含任何数据。
	// 
	// 读取zip文件时，直接从zip文件填充名称字段，不验证其正确性。
	// 调用方有责任将其清理为
	// 适当，包括规范化斜杠方向，
	// 验证路径是否相对，以及防止路径
	// 通过文件名（“../../”）进行遍历。
	Name string

	// Comment是任何短于64KiB的任意用户定义字符串。
	Comment string

	// NonUTF8表示名称和注释没有用UTF-8编码。
	// 
	// 根据规范，唯一允许的其他编码应该是CP-437，
	// 但历史上许多ZIP阅读器将名称和注释解释为系统的本地字符编码。
	// 
	// 仅当用户打算为特定本地化区域的非便携
	// ZIP文件编码时，才应设置此标志。否则，编写器
	// 会自动为有效的UTF-8字符串设置ZIP格式的UTF-8标志。
	NonUTF8 bool

	CreatorVersion uint16
	ReaderVersion  uint16
	Flags          uint16

	// 方法是压缩方法。如果为零，则使用存储。
	Method uint16

	// Modified是文件的修改时间。
	// 
	// 读取时，与传统MS-DOS 
	// 日期字段相比，优先使用扩展时间戳，时间之间的偏移量用作时区。
	// 如果只存在MS-DOS日期，则假定时区为UTC。
	// 
	// 写入时，始终会发出一个扩展时间戳（与时区无关）。传统MS-DOS日期字段根据修改时间的
	// 位置进行编码。
	Modified     time.Time
	ModifiedTime uint16 // 已弃用：旧版MS-DOS日期；改用修改过的。
	ModifiedDate uint16 // 已弃用：遗留MS-DOS时间；改用修改过的。

	CRC32              uint32
	CompressedSize     uint32 // 不推荐使用：改用CompressedSize64。
	UncompressedSize   uint32 // 不推荐使用：改用未压缩的ize64。
	CompressedSize64   uint64
	UncompressedSize64 uint64
	Extra              []byte
	ExternalAttrs      uint32 // 含义取决于创建者版本
}

// FileInfo返回一个fs。文件头的文件信息。
func (h *FileHeader) FileInfo() fs.FileInfo {
	return headerFileInfo{h}
}

// headerFileInfo实现fs。文件信息。
type headerFileInfo struct {
	fh *FileHeader
}

func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) }
func (fi headerFileInfo) Size() int64 {
	if fi.fh.UncompressedSize64 > 0 {
		return int64(fi.fh.UncompressedSize64)
	}
	return int64(fi.fh.UncompressedSize)
}
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
func (fi headerFileInfo) ModTime() time.Time {
	if fi.fh.Modified.IsZero() {
		return fi.fh.ModTime()
	}
	return fi.fh.Modified.UTC()
}
func (fi headerFileInfo) Mode() fs.FileMode { return fi.fh.Mode() }
func (fi headerFileInfo) Type() fs.FileMode { return fi.fh.Mode().Type() }
func (fi headerFileInfo) Sys() any          { return fi.fh }

func (fi headerFileInfo) Info() (fs.FileInfo, error) { return fi, nil }

// FileInfoHeader从
// fs创建一个部分填充的FileHeader。文件信息。因为fs。FileInfo的Name方法只返回其描述的文件的基本名称
// 可能需要修改返回头的Name字段
// 以提供文件的完整路径名。
// 如果需要压缩，调用方应该设置文件头。方法
// /场；默认情况下，它是未设置的。
func FileInfoHeader(fi fs.FileInfo) (*FileHeader, error) {
	size := fi.Size()
	fh := &FileHeader{
		Name:               fi.Name(),
		UncompressedSize64: uint64(size),
	}
	fh.SetModTime(fi.ModTime())
	fh.SetMode(fi.Mode())
	if fh.UncompressedSize64 > uint32max {
		fh.UncompressedSize = uint32max
	} else {
		fh.UncompressedSize = uint32(fh.UncompressedSize64)
	}
	return fh, nil
}

type directoryEnd struct {
	diskNbr            uint32 // 未使用的
	dirDiskNbr         uint32 // 未使用的
	dirRecordsThisDisk uint64 // 未使用的
	directoryRecords   uint64
	directorySize      uint64
	directoryOffset    uint64 // 相对于文件
	commentLen         uint16
	comment            string
}

// 时区返回*时间。基于提供的偏移量的位置。
// 如果偏移量不合理，则使用零偏移量。
func timeZone(offset time.Duration) *time.Location {
	const (
		minOffset   = -12 * time.Hour  // 例如，贝克岛在-12:00 
		maxOffset   = +14 * time.Hour  // 例如，Line岛在+14:00 
		offsetAlias = 15 * time.Minute // 例如，尼泊尔在+5:45 
	)
	offset = offset.Round(offsetAlias)
	if offset < minOffset || maxOffset < offset {
		offset = 0
	}
	return time.FixedZone("", int(offset/time.Second))
}

// MSDOSTIMETIME将MS-DOS日期和时间转换为时间。时间
// 分辨率为2秒。
// 见：https:
func msDosTimeToTime(dosDate, dosTime uint16) time.Time {
	return time.Date(
		// 日期位0-4：月日；5-8个月；9-15:1980年以来的年份
		int(dosDate>>9+1980),
		time.Month(dosDate>>5&0xf),
		int(dosDate&0x1f),

		// 时间位0-4:second/2；5-10分钟；11-15:hour 
		int(dosTime>>11),
		int(dosTime>>5&0x3f),
		int(dosTime&0x1f*2),
		0, // 纳秒

		time.UTC,
	)
}

// timeToMsDosTime转换时间。MS-DOS日期和时间的时间。
// 分辨率为2秒。
// 见：https:
func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
	fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
	fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
	return
}

// ModTime使用传统
// ModifiedDate和ModifiedTime字段以UTC为单位返回修改时间。
// 
// 已弃用：改用Modified。
func (h *FileHeader) ModTime() time.Time {
	return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
}

// SetModTime将Modified、ModifiedTime和ModifiedState字段
// 设置为UTC中的给定时间。
// 
// 已弃用：改用Modified。
func (h *FileHeader) SetModTime(t time.Time) {
	t = t.UTC() // 转换为UTC以实现兼容性
	h.Modified = t
	h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
}

const (
	// Unix常量。规范中没有提到它们，但这些似乎是工具商定的值。
	s_IFMT   = 0xf000
	s_IFSOCK = 0xc000
	s_IFLNK  = 0xa000
	s_IFREG  = 0x8000
	s_IFBLK  = 0x6000
	s_IFDIR  = 0x4000
	s_IFCHR  = 0x2000
	s_IFIFO  = 0x1000
	s_ISUID  = 0x800
	s_ISGID  = 0x400
	s_ISVTX  = 0x200

	msdosDir      = 0x10
	msdosReadOnly = 0x01
)

// Mode返回文件头的权限和模式位。
func (h *FileHeader) Mode() (mode fs.FileMode) {
	switch h.CreatorVersion >> 8 {
	case creatorUnix, creatorMacOSX:
		mode = unixModeToFileMode(h.ExternalAttrs >> 16)
	case creatorNTFS, creatorVFAT, creatorFAT:
		mode = msdosModeToFileMode(h.ExternalAttrs)
	}
	if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
		mode |= fs.ModeDir
	}
	return mode
}

// SetMode更改文件头的权限和模式位。
func (h *FileHeader) SetMode(mode fs.FileMode) {
	h.CreatorVersion = h.CreatorVersion&0xff | creatorUnix<<8
	h.ExternalAttrs = fileModeToUnixMode(mode) << 16

	// 也设置MSDOS属性，就像原来的zip一样。
	if mode&fs.ModeDir != 0 {
		h.ExternalAttrs |= msdosDir
	}
	if mode&0200 == 0 {
		h.ExternalAttrs |= msdosReadOnly
	}
}

// isZip64报告文件大小是否超过32位限制
func (h *FileHeader) isZip64() bool {
	return h.CompressedSize64 >= uint32max || h.UncompressedSize64 >= uint32max
}

func (f *FileHeader) hasDataDescriptor() bool {
	return f.Flags&0x8 != 0
}

func msdosModeToFileMode(m uint32) (mode fs.FileMode) {
	if m&msdosDir != 0 {
		mode = fs.ModeDir | 0777
	} else {
		mode = 0666
	}
	if m&msdosReadOnly != 0 {
		mode &^= 0222
	}
	return mode
}

func fileModeToUnixMode(mode fs.FileMode) uint32 {
	var m uint32
	switch mode & fs.ModeType {
	default:
		m = s_IFREG
	case fs.ModeDir:
		m = s_IFDIR
	case fs.ModeSymlink:
		m = s_IFLNK
	case fs.ModeNamedPipe:
		m = s_IFIFO
	case fs.ModeSocket:
		m = s_IFSOCK
	case fs.ModeDevice:
		m = s_IFBLK
	case fs.ModeDevice | fs.ModeCharDevice:
		m = s_IFCHR
	}
	if mode&fs.ModeSetuid != 0 {
		m |= s_ISUID
	}
	if mode&fs.ModeSetgid != 0 {
		m |= s_ISGID
	}
	if mode&fs.ModeSticky != 0 {
		m |= s_ISVTX
	}
	return m | uint32(mode&0777)
}

func unixModeToFileMode(m uint32) fs.FileMode {
	mode := fs.FileMode(m & 0777)
	switch m & s_IFMT {
	case s_IFBLK:
		mode |= fs.ModeDevice
	case s_IFCHR:
		mode |= fs.ModeDevice | fs.ModeCharDevice
	case s_IFDIR:
		mode |= fs.ModeDir
	case s_IFIFO:
		mode |= fs.ModeNamedPipe
	case s_IFLNK:
		mode |= fs.ModeSymlink
	case s_IFREG:
		// 
	case s_IFSOCK:
		mode |= fs.ModeSocket
	}
	if m&s_ISGID != 0 {
		mode |= fs.ModeSetgid
	}
	if m&s_ISUID != 0 {
		mode |= fs.ModeSetuid
	}
	if m&s_ISVTX != 0 {
		mode |= fs.ModeSticky
	}
	return mode
}
